/*
 * OpenParts
 * A dynamic-loading components framework for GWT
 * Copyright (C) 2011 Christophe Bouthier  [chris{AT}binary-gastronome{DOT}fr]
 *
 * This work is partially based on work I have done at INRIA (http://www.inria.fr) 
 * in the context of the Qualipso European Project (http://qualipso.org/),
 * The work done at INRIA is Copyright (C) 2006-2011 INRIA
 *
 * This work is distributed under the LGPL version 3
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation. See the GNU
 * Lesser General Public License in LGPL.txt for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package fr.openparts.OpenParts.client;

import java.util.HashMap;
import java.util.Map;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.RootPanel;

/**
 * Parts loader, doing the effective loading of parts and registering them.
 * 
 * @author Christophe Bouthier [chris{AT}binary-gastronome{DOT}fr]
 * @creation.date 24 November 2010
 */
public class OPPartsLoader {

    /**
     * A part can be in one and only one of those state:
     * <dl>
     * <dt>NOT_LOADED</dt>
     * <dd>there is no script element for this part in the page</dd>
     * <dt>LOAD_IN_PROGRESS</dt>
     * <dd>there is a script element for this part in the page, but there have been neither notification of completion nor timeout. This should be a transitive
     * state.</dd>
     * <dt>LOADED</dt>
     * <dd>the part is officially loaded: there is a script element and there have been a notification of completion</dd>
     * <dt>UNSUCCESSFULL_LOADING</dt>
     * <dd>there is a script element for this part in the page, but there have been a timeout on it.</dd>
     * </dl>
     */
    public enum PartState {
        NOT_LOADED, 
        LOAD_IN_PROGRESS, 
        LOADED, 
        UNSUCCESSFULL_LOADING
    }

    /**
     * Timeout for loading a new part.
     */
    protected static final int LOADING_TIMEOUT = 5000;

    /**
     * Timer associated with the request loading in progress, to manage timeout.
     */
    protected Timer loadingTimeoutTimer;

    /**
     * If a loading is in progress, the request corresponding to this loading ; null if there is no loading in progress right now.
     */
    protected OPPartLoadingRequest requestInProgress;

    /**
     * States of the parts for which at least one loading request was done.
     */
    protected Map<String, PartState> partsStates = new HashMap<String, OPPartsLoader.PartState>();

    /**
     * Create and bind the parts loader module.
     */
    public final static void bind() {
        final OPPartsLoader partsLoader = new OPPartsLoader();
        final OPPartsLoaderBinding partsLoaderBinding = OPPartsLoaderBinding.partsLoaderBindingFactory();

        partsLoader.registerMeAsBinding(partsLoaderBinding);
        OPShell.opBindingSharedInstance().bind(partsLoaderBinding);
    }

    /**
     * Register this parts loader as the parts loader of the given parts loader binding.
     * 
     * @param partsLoaderBinding
     *            the parts loader binding to register into. Shouldn't be null.
     */
    private void registerMeAsBinding(final OPPartsLoaderBinding partsLoaderBinding) {
        registerMeAsExecuteLoadingRequestHandler(this, partsLoaderBinding);
        registerMeAsNotifyLoadingCompletedHandler(this, partsLoaderBinding);
        registerMeAsIsPartLoadedHandler(this, partsLoaderBinding);
    }

    /**
     * Protected constructor, should use the bind static method.
     */
    protected OPPartsLoader() {
    }

    /**
     * Register the given parts loader as the executor of loading request for the given parts loader binding.
     * 
     * @param partsLoader
     *            the given parts loader to register as handler for executing loading request
     * @param partsLoaderBinding
     *            the given parts loader binding on which to register the parts loader
     */
    protected final native void registerMeAsExecuteLoadingRequestHandler(final OPPartsLoader partsLoader, final OPPartsLoaderBinding partsLoaderBinding) /*-{
        partsLoaderBinding.executeLoadingRequest = function(request) {
            return $entry(partsLoader.@fr.openparts.OpenParts.client.OPPartsLoader::executeLoadingRequest(Lfr/openparts/OpenParts/client/OPPartLoadingRequest;)(request));
        };
    }-*/;

    /**
     * Register the given parts loader as the handler of part loading completion notification for the given parts loader binding.
     * 
     * @param partsLoader
     *            the given parts loader to register as handler for notification when a part completed loading
     * @param partsLoaderBinding
     *            the given parts loader binding on which to register the parts loader
     */
    protected final native void registerMeAsNotifyLoadingCompletedHandler(final OPPartsLoader partsLoader, final OPPartsLoaderBinding partsLoaderBinding) /*-{
        partsLoaderBinding.notifyLoadingCompleted = function(service) {
            return $entry(partsLoader.@fr.openparts.OpenParts.client.OPPartsLoader::notifyLoadingCompleted(Ljava/lang/String;)(service));
        };
    }-*/;

    /**
     * Register the given parts loader as the handler of check if a part is loaded for the given parts loader binding.
     * 
     * @param partsLoader
     *            the given parts loader to register as handler of check if a part is loaded
     * @param partsLoaderBinding
     *            the given parts loader binding on which to register the parts loader
     */
    protected final native void registerMeAsIsPartLoadedHandler(final OPPartsLoader partsLoader, final OPPartsLoaderBinding partsLoaderBinding) /*-{
        partsLoaderBinding.isPartLoaded = function(service) {
            // here, don't encapsulate in $entry() because $entry is returning a JSObject, which we don't want, we just want a boolean
            return partsLoader.@fr.openparts.OpenParts.client.OPPartsLoader::isPartLoaded(Ljava/lang/String;)(service);
        };
    }-*/;

    /**
     * Load the part corresponding to the given service. If the part is correctly loaded, the onSuccess method of the callback is called. If the part is not
     * loaded before the timeout, the onTimeout method of the callback is called.
     * 
     * @param request
     *            the load part request to execute. Shouldn't be null.
     */
    public void executeLoadingRequest(final OPPartLoadingRequest request) {
        assert request != null : "parameter request shouldn't be null";

        // if the part is already officially loaded, just call back the success callback
        if (isPartLoaded(request.getService())) {
            // here, the execution was done synchronously, so flag the request as such
            request.setAsHavingBeenExecutedSynchronously();
            request.onSuccess();
            return;
        }

        // set the initial state of the service
        partsStates.put(request.getService(), PartState.NOT_LOADED);

        // set the request as the one in progress
        requestInProgress = request;

        // add the script element to the html page, thus loading the part
        addLoadingScript(request.getService());

        // ok so loading is in progress, set the corresponding state of the service
        partsStates.put(request.getService(), PartState.LOAD_IN_PROGRESS);

        // launch a timer for the timeout
        loadingTimeoutTimer = new Timer() {
            @Override
            public void run() {
                loadingTimeout(request);
            }
        };
        loadingTimeoutTimer.schedule(LOADING_TIMEOUT);
    }

    /**
     * Called when a part finished loading and binding itself. This method is called asynchronously, when the service is loaded. This method should not be
     * called directly, it is called by {@link OPShell}.
     * 
     * @param service
     *            the name of the service of the part that finished loading. Shouldn't be null.
     */
    public void notifyLoadingCompleted(final String service) {
        assert service != null : "parameter service shouldn't be null";

        // check that the service is indeed in a load in progress state.
        // there can be a problem if the timeout arrives just before the notification. If this is the case, just ignore the notification
        if (!PartState.LOAD_IN_PROGRESS.equals(getPartState(service))) {
            return;
        }

        // stop the timer
        assert loadingTimeoutTimer != null : "no timer in progress";
        loadingTimeoutTimer.cancel();
        loadingTimeoutTimer = null;

        // ok, so here we're sure that the service is officially loaded
        partsStates.put(service, PartState.LOADED);

        // remove the request in progress
        OPPartLoadingRequest request = requestInProgress;
        requestInProgress = null;

        // call the success callback
        request.onSuccess();
    }

    /**
     * Timeout loading the part. Remove everything and call timeout callback.
     * 
     * @param request
     *            the request corresponding to the part to load. Shouldn't be null.
     */
    protected void loadingTimeout(final OPPartLoadingRequest request) {
        assert request != null : "parameter request shouldn't be null";

        // check if we are still in loading. If not, then the notification came just before the timeout, so ignore the notification
        if (!PartState.LOAD_IN_PROGRESS.equals(getPartState(request.getService()))) {
            return;
        }
        
        // the service didn't load successfully
        partsStates.put(request.getService(), PartState.UNSUCCESSFULL_LOADING);

        // clean up the mess
        requestInProgress = null;
        loadingTimeoutTimer = null;
        removeLoadingScript(request.getService());

        // reinit the state of the service
        partsStates.put(request.getService(), PartState.NOT_LOADED);

        // call timeout callback
        request.onTimeout();
    }

    /**
     * Returns true if the given service is already loaded, false otherwise;
     * 
     * @param service
     *            the service name to check. Shouldn't be null.
     * @return true if the given part is already loaded, false otherwise
     */
    public boolean isPartLoaded(final String service) {
        assert service != null : "parameter service shouldn't be null";

        PartState state = getPartState(service);
        if (PartState.LOADED.equals(state)) {
            return true;
        }
        return false;
    }

    /**
     * Returns the state of the part.
     * 
     * @param service
     *            the service name for the checked part
     * @return the state of the part. @see {@link PartState} for more information.
     */
    protected PartState getPartState(final String service) {
        assert service != null : "parameter service shouldn't be null";

        if (!partsStates.containsKey(service)) {
            partsStates.put(service, PartState.NOT_LOADED);
        }

        return partsStates.get(service);
    }

    /**
     * Add a script element containing the part loader for the given service. Call {@link #createLoadingScript(String)} to create the script to add.
     * 
     * @param service
     *            the name of the service part to dynamically load. Shouldn't be null.
     */
    protected void addLoadingScript(final String service) {
        assert service != null : "parameter service shoudln't be null";

        DOM.appendChild(RootPanel.getBodyElement(), createLoadingScript(service));
    }

    /**
     * Create a script element containing the part loader for the given service. Call {@link OPShell#partLoaderUrlForService(String)} to get the url of the part
     * loader js file, and {@link OPShell#partLoaderIdForService(String)} to get the ID to attach to the script.
     * 
     * @param service
     *            the name of the service part to dynamically load. Shouldn't be null.
     * @return the script element containing the loading js file. Cannot be null.
     */
    protected Element createLoadingScript(final String service) {
        assert service != null : "parameter service shouldn't be null";

        final Element scriptElement = DOM.createElement("script");
        assert scriptElement != null;

        scriptElement.setAttribute("type", "text/javascript");
        scriptElement.setAttribute("language", "javascript");
        scriptElement.setAttribute("src", OPShell.partLoaderUrlForService(service));
        scriptElement.setAttribute("id", OPShell.partLoaderIdForService(service));

        return scriptElement;
    }

    /**
     * Remove the script element containing the part loader for the given service. Don't do anything if there is no such script element.
     * 
     * @param service
     *            the name of the service of the part to remove. Shouldn't be null.
     */
    protected void removeLoadingScript(final String service) {
        assert service != null : "parameter service shouldn't be null";

        final Element scriptElement = DOM.getElementById(OPShell.partLoaderIdForService(service));
        if (scriptElement == null) {
            return;
        }

        DOM.removeChild((Element) scriptElement.getParentElement(), scriptElement);
    }

}
